package com.sina.shop.util;

import org.apache.commons.codec.binary.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.ResourcePatternResolver;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.io.*;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

/**
 * 这个为用于 密钥的生成，写出，和读入
 */
public abstract class KeyUtils {

    private static final Logger LOG = LoggerFactory.getLogger(KeyUtils.class);

    private static final String RSA = "RSA";

    private KeyUtils() {

    }

    public static PublicKey readPublicKey(ResourcePatternResolver resolver, String resourcePattern) throws Exception {
        Resource[] resources = resolver.getResources(resourcePattern);
        LOG.debug("{} count {}", resourcePattern, resources.length);
        return readPublicKey(resources[0]);
    }

    public static PublicKey[] readPublicKeyArray(ResourcePatternResolver resolver, String resourcePattern)throws Exception{
        Resource[] resources = resolver.getResources(resourcePattern);
        LOG.debug("{} count {}", resourcePattern, resources.length);
        PublicKey[] publicKeys = new PublicKey[resources.length];
        for (int i = 0; i < resources.length; i++) {
            publicKeys[i] = readPublicKey(resources[i]);
        }
        return publicKeys;
    }

    public static PublicKey readPublicKey(Resource resource)throws IOException,RuntimeException{
        LOG.debug("public key file {}", resource.getFilename());
        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(resource.getInputStream()))) {
            char[] keyChars = new char[1024];
            StringBuilder builder = new StringBuilder();
            int len;
            while ((len = reader.read(keyChars)) > 0) {
                builder.append(keyChars, 0, len);
            }
            String keyStr = builder.toString();
            keyStr = keyStr.replaceAll("(?:-+BEGIN PUBLIC KEY-+\\r?\\n|-+END PUBLIC KEY-+\\r?\\n?)", "");
            byte[] keyBytes = Base64.decodeBase64(keyStr);
            X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            PublicKey publicKey;
            publicKey = keyFactory.generatePublic(spec);
            LOG.trace("load publicKey{} success ", keyStr);
            return publicKey;
        } catch (FileNotFoundException e) {
            LOG.debug(" {}", e.getMessage());
            throw new RuntimeException(String.format("没有文件 %s ",resource.getFilename()));
        } catch (IOException e) {
            throw new RuntimeException("read publicKey key file error.");
        } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
            LOG.error("publicKey create error.", e);
            throw new RuntimeException(e);
        }
    }

    public static PublicKey readPublicKey(String keyStr) {
        // TODO hehaiyuan
        keyStr = keyStr.replaceAll("(?:-+BEGIN PUBLIC KEY-+\\r?\\n?|-+END PUBLIC KEY-+\\r?\\n?)", "");
        byte[] keyBytes = Base64.decodeBase64(keyStr);
        X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = null;
        try {
            keyFactory = KeyFactory.getInstance("RSA");
        } catch (NoSuchAlgorithmException e) {
            // this can't be happen
            LOG.error("publicKey create error.", e);
        }
        PublicKey publicKey;
        try {
            publicKey = keyFactory.generatePublic(spec);
        } catch (InvalidKeySpecException e) {
            throw new RuntimeException("密钥格式不正确:" + e);
        }
        LOG.trace("load publicKey{} success ", keyStr);
        return publicKey;
    }


    public static boolean isPublicKey(String keyStr){
        boolean v;
        try {
            PublicKey publicKey;
            publicKey = readPublicKey( keyStr );
            v = publicKey != null;
        } catch (Exception e) {
            v = false;
        }
        return v;
    }

    public static PrivateKey readPrivateKey(String keyStr) {
        keyStr = keyStr.replaceAll("(?:-+BEGIN PRIVATE KEY-+\\r?\\n|-+END PRIVATE KEY-+\\r?\\n?)", "");
        byte[] keyBytes = Base64.decodeBase64(keyStr);
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = null;
        try {
            keyFactory = KeyFactory.getInstance("RSA");
            PrivateKey privateKey;
            privateKey = keyFactory.generatePrivate(spec);
            LOG.trace("load private key{} success", keyStr);
            return privateKey;
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (InvalidKeySpecException e) {
            e.printStackTrace();
        }
        return null;
    }

    public static PrivateKey readPrivateKey(Resource resource)throws IOException,RuntimeException{
        LOG.debug("key file {} ", resource.getFilename());
        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(resource.getInputStream()))) {
            char[] keyChars = new char[1024];
            StringBuilder builder = new StringBuilder();
            int len;
            while ((len = reader.read(keyChars)) > 0) {
                builder.append(keyChars, 0, len);
            }
            String keyStr = builder.toString();
            keyStr = keyStr.replaceAll("(?:-+BEGIN PRIVATE KEY-+\\r?\\n|-+END PRIVATE KEY-+\\r?\\n?)", "");
            byte[] keyBytes = Base64.decodeBase64(keyStr);
            PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            PrivateKey privateKey;
            privateKey = keyFactory.generatePrivate(spec);
            LOG.trace("load private key{} success", keyStr);
            return privateKey;
        } catch (FileNotFoundException e) {
            LOG.debug(" {}", e.getMessage());
            throw new RuntimeException(String.format("没有文件 %s ",resource.getFilename()));
        } catch (IOException e) {
            throw new RuntimeException("read private key file error.");
        } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
            LOG.error("private create error.", e);
            throw new RuntimeException(e);
        }
    }

    public static PrivateKey[] readPrivateKeyArray(ResourcePatternResolver resolver, String resourcePattern)
            throws RuntimeException{
        try {
            Resource[] resources = resolver.getResources(resourcePattern);
            LOG.debug("{} count {}", resourcePattern, resources.length);
            PrivateKey[] privateKeys = new PrivateKey[resources.length];
            for (int i = 0; i < resources.length; i++) {
                privateKeys[i] = readPrivateKey(resources[i]);
            }
            return privateKeys;
        } catch (Exception e) {
           throw new RuntimeException(e);
        }
    }


    public static PrivateKey readPrivateKey(ResourcePatternResolver resolver, String resourcePattern) throws IOException {
        Resource[] resources = resolver.getResources(resourcePattern);
        LOG.debug("{} count {}", resourcePattern, resources.length);
        return readPrivateKey(resources[0]);
    }

    public static String[] readKeyStringArray(ResourcePatternResolver resolver, String resourcePattern)
            throws RuntimeException{
        try {
            Resource[] resources = resolver.getResources(resourcePattern);
            String[] keyStringArray = new String[resources.length];
            for (int i = 0; i < resources.length; i++) {
                keyStringArray[i] = readKeyString(resources[i] );
            }
            return keyStringArray;
        } catch (Exception e) {
           throw new RuntimeException(e);
        }
    }

    public static String readKeyString(Resource resource)throws RuntimeException{
        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(resource.getInputStream()))) {
            char[] keyChars = new char[1024];
            StringBuilder builder = new StringBuilder();
            int len;
            while ((len = reader.read(keyChars)) > 0) {
                builder.append(keyChars, 0, len);
            }
           return builder.toString()
                   .replaceAll("(?:-+BEGIN (?:PRIVATE|PUBLIC) KEY-+\\r?\\n|-+END (?:PRIVATE|PUBLIC) KEY-+\\r?\\n?)", "");

        } catch (FileNotFoundException e) {
            LOG.debug(" {}", e.getMessage());
            throw new RuntimeException(String.format("没有文件 %s ",resource.getFilename()));
        } catch (IOException e) {
            throw new RuntimeException("read private key file error.");
        }
    }

    /**
     * 验证签名
     *
     * @return true 表示成功
     */
    public static boolean verifySignatureWithRSA(String content, String signature, PublicKey publicKey) {
        return verifySignatureWithRSA(content.getBytes(Constant.CHARSET_UTF8), signature, publicKey);
    }

    public static boolean verifySignatureWithRSA(byte[] contentBytes,String signature, PublicKey publicKey){
        boolean valid = false;
        try {
            Signature signatureObj = Signature.getInstance("SHA256withRSA");
            signatureObj.initVerify(publicKey);
            signatureObj.update(contentBytes);
            valid = signatureObj.verify(Base64.decodeBase64(signature));
        } catch (NoSuchAlgorithmException e) {
            LOG.error("签名验证算法错误.", e);
        } catch (InvalidKeyException e) {
            LOG.error("public key invalid ", e);
        } catch (SignatureException e) {
            LOG.error("signature exception. ", e);
        } catch (Exception e){
            LOG.error("",e);
        }
        return valid;
    }

    /**
     * 获取签名
     *
     * @return 若签名成功则返回签名结果,否则返回 null
     */
    public static String signatureWithRSA(String content, PrivateKey privateKey) {
        return signatureWithRSA(content.getBytes(Constant.CHARSET_UTF8),privateKey);
    }

    public static String signatureWithRSA(byte[] contentBytes,PrivateKey privateKey){
        String signature = null;
        try {
            Signature signatureObj = Signature.getInstance("SHA256withRSA");
            signatureObj.initSign(privateKey);
            signatureObj.update(contentBytes);
            signature = Base64.encodeBase64String(signatureObj.sign());
        } catch (NoSuchAlgorithmException e) {
            LOG.error("签名验证算法错误.", e);
        } catch (InvalidKeyException e) {
            LOG.error("private key invalid ", e);
        } catch (SignatureException e) {
            LOG.error("signature exception. ", e);
        } catch (Exception e) {
            LOG.error("未知错误，请检查代码或依赖库的版本",e);
        }
        return signature;
    }

    /**
     * 以 RSA 算法加密文本,字符集为UTF-8
     * @param rawText 要加密的文本,转换为字节后长度不大于 [key 的大小的 / 8 - 11]
     *                ,如: key 的大小的 2048 那么 rawText 转换为字节后长度不大于 245
     * @param key 公钥或私钥
     * @return 密文字节,若失败则返回 null
     * @throws IllegalArgumentException - 参数错误
     */
    public static byte[] encryptWithRsa(String rawText,Key key){
        Assert.notNull(rawText,"rawText required");
        assertRsaKey(key);
        byte[] bytes = null;
        try {
            Cipher cipher = Cipher.getInstance(RSA);
            cipher.init(Cipher.ENCRYPT_MODE, key);
            bytes =  cipher.doFinal(rawText.getBytes(Constant.UTF8));
        } catch (NoSuchAlgorithmException e) {
            LOG.error("JDK 不支持 算法[{}].",RSA);
        }catch (InvalidKeyException e) {
           LOG.error("public error.",e);
        } catch (BadPaddingException e) {
            LOG.error("JDK 内部提供者错误",e);
        } catch (UnsupportedEncodingException e) {
           LOG.error("JDK 不支持字符集[{}]", Constant.UTF8,e);
        } catch (IllegalBlockSizeException e) {
            LOG.error("加密文本[{}]不在 public key 加密的范围内",rawText,e);
        } catch (NoSuchPaddingException e) {
            // 加密模式不会到这里
            e.printStackTrace();
        }
        return bytes;
    }

    /**
     * 断言 key 为 RSA 算法的公钥或私钥
     * @param key RSA 算法的公钥或私钥
     * @throws IllegalArgumentException - key 不是 RSA 算法的公钥或私钥
     */
    public static void assertRsaKey(Key key)throws IllegalArgumentException{
        Assert.notNull(key,"key required");
        if(!(key instanceof PublicKey) && !(key instanceof PrivateKey)){
            throw new IllegalArgumentException("密钥不是私钥也不是公钥");
        }
        if(!RSA.equals(key.getAlgorithm())){
            throw new IllegalArgumentException(String.format("密钥算法[%s]不是[%s]",key.getAlgorithm(),RSA));
        }
    }

    /**
     * 以 RSA 算法加密文本,字符集为UTF-8
     * @param rawText 要加密的文本
     * @param key 公钥
     * @return 密文字节,若失败则返回 null
     * @see #encryptWithRsa(String, Key)
     */
    public static String encryptWithRsaAsBase64(String rawText,Key key){
        byte[] bytes = encryptWithRsa(rawText, key);
        String encodedText;
        if(bytes != null){
            encodedText = Base64.encodeBase64String(bytes);
        }else {
            encodedText = null;
        }
        return encodedText;
    }

    /**
     * 以 RSA 算法解密文本,字符集为UTF-8
     * @param base64Text 要解密的base64文本,base64解码并转换为字节后长度不大于 [key 的大小的 / 8 ]
     *                ,如: key 的大小的 2048 那么 rawText 转换为字节后长度不大于 246
     * @param key 公钥
     * @return 明文字节,若失败则返回 null
     * @throws IllegalArgumentException - 参数错误
     */
    public static byte[] decryptWithRsa(String base64Text,Key key){
        Assert.notNull(base64Text,"base64Text required");
        assertRsaKey(key);
        byte[] bytes = null;
        try {
            Cipher cipher = Cipher.getInstance(RSA);
            cipher.init(Cipher.DECRYPT_MODE, key);
            bytes =  cipher.doFinal(Base64.decodeBase64(base64Text));
        } catch (NoSuchAlgorithmException e) {
            LOG.error("JDK 不支持 算法[{}].",RSA);
        }catch (InvalidKeyException e) {
            LOG.error("public error.",e);
        } catch (BadPaddingException e) {
            LOG.error("JDK 内部提供者错误",e);
        } catch (IllegalBlockSizeException e) {
            LOG.error("解密文本[{}]不在 public key 解密的范围内",base64Text,e);
        } catch (NoSuchPaddingException e) {
           LOG.error("base64 表示的密文错误",e);
        }
        return bytes;
    }

    /**
     * 以 RSA 算法解密文本,字符集为UTF-8
     * @param base64Text 要解密的base64文本
     * @param key 公钥
     * @return 明文字节,若失败则返回 null
     * @see #decryptWithRsa(String, Key)
     * @throws IllegalArgumentException - 参数错误
     */
    public static String decryptWithRsaAsString(String base64Text,Key key){
        byte[] bytes = decryptWithRsa(base64Text, key);
        String rawText;
        if(bytes != null){
            try {
                rawText = new String(bytes, Constant.UTF8);
            } catch (UnsupportedEncodingException e) {
                // 不会到这里，都支持 UTF-8
                rawText = null;
                LOG.error("JDK 不支持字符集[{}]", Constant.UTF8,e);
            }
        }else {
            rawText = null;
        }
        return rawText;
    }



    /**
     * 创建公钥和私钥对
     *
     * @param keySize 密钥大小
     * @param random  可null的随机参数
     * @return [0] 公钥,[1] 私钥
     */
    public static Key[] createRSAKeyPair(int keySize, SecureRandom random) {
        try {
            KeyPairGenerator keyPairGen = KeyPairGenerator
                    .getInstance(RSA);
            if (random == null) {
                keyPairGen.initialize(keySize);
            } else {
                keyPairGen.initialize(keySize, random);
            }
            KeyPair keyPair = keyPairGen.generateKeyPair();
            return new Key[]{keyPair.getPublic(), keyPair.getPrivate()};
        } catch (NoSuchAlgorithmException e) {
            // 算法正确,不会到这里
            LOG.error("", e);
            throw new RuntimeException(e);
        }
    }

    /**
     * 这个方法 只是将 {@link #createRSAKeyPair(int, SecureRandom)} 的结果转换为 Base64 的字符串
     *
     * @see #createRSAKeyPair(int, SecureRandom)
     */
    public static String[] createRSAKeyPairString(int keySize, SecureRandom random) {
        Key[] keys = createRSAKeyPair(keySize, random);
        return new String[]{
                Base64.encodeBase64String(keys[0].getEncoded())
                , Base64.encodeBase64String(keys[1].getEncoded())
        };
    }

    /**
     * 创建 公钥和私钥并写入文件
     *
     * @param keySize 密钥尺寸
     * @param random  安全随机数,可 null
     * @param files   一个长度为2的数组,要写入公钥和私钥的文件,[0]写入公钥,[1]写入私钥
     * @throws IOException - 创建文件或写入错误
     */
    public static void createRSAKeyPairFile(int keySize, SecureRandom random, File[] files) throws IOException {
        String[] keyStrings = createRSAKeyPairString(keySize, random);
        for (File file : files) {
            if (!file.exists() && !file.createNewFile()) {
                LOG.info("文件[{}]创建失败", file.getAbsolutePath());
            }
        }
        try (BufferedWriter publicWriter = new BufferedWriter(new FileWriter(files[0]));
             BufferedWriter privateWriter = new BufferedWriter(new FileWriter(files[1]))) {
            //写入公钥
            writeToFile(publicWriter, keyStrings[0], false);
            // 写入私钥
            writeToFile(privateWriter, keyStrings[1], true);
        }

    }

    /**
     * 将公钥或私钥的内容写入相应的文件
     *
     * @param content    密钥内容
     * @param privateKey true 写入的私钥,false 写入的公钥
     * @throws IOException - 定入错误
     */
    private static void writeToFile(BufferedWriter writer, final String content, boolean privateKey)
            throws IOException {
        int size = 64, start, end;
        int count = content.length() / size;
        count = content.length() % size == 0 ? count : count + 1;
        writer.write(String.format("-----BEGIN %s KEY-----", privateKey ? "PRIVATE" : "PUBLIC"));
        writer.newLine();
        for (int i = 0; i < count; i++) {
            start = i * size;
            end = start + size;
            end = Math.min(end, content.length());
            writer.write(content.substring(start, end));
            writer.newLine();
        }
        writer.write(String.format("-----END %s KEY-----", privateKey ? "PRIVATE" : "PUBLIC"));
    }


}
